use std::{
	cell::{Cell, RefCell},
	rc::Rc,
};

use web_sys::{
	js_sys::Array, wasm_bindgen::prelude::*, CompositionEvent, HtmlTextAreaElement, KeyboardEvent, MouseEvent, ResizeObserver, WheelEvent,
};

use thunderdome::{Arena, Index};
use web_sys::{Document, HtmlCanvasElement, HtmlElement, Window as HtmlWindow};

use crate::win::WinKeyboard;

use super::{WinHandle, WinMouse, WinRef, WinWheel, Window};

pub type RcDynWinDelegate = Rc<dyn types::WinRep<Window>>;

pub struct Win {
	pub(super) key_: String,
	rep: RcDynWinDelegate,
	pub canvas: HtmlCanvasElement,
	pub window: HtmlWindow,
	pub document: Document,
	pub body: HtmlElement,
	pub cursor: HtmlTextAreaElement,
	frame: Frame,
	window_resize: Option<(Closure<dyn Fn()>, Rc<Closure<dyn Fn()>>)>,
	_canvas_resize: Closure<dyn Fn(Array, ResizeObserver)>,
	on_keydown: Closure<dyn Fn(KeyboardEvent)>,
	on_keyup: Closure<dyn Fn(KeyboardEvent)>,
	on_mousedown: Closure<dyn Fn(MouseEvent)>,
	on_mouseup: Closure<dyn Fn(MouseEvent)>,
	on_mousemove: Closure<dyn Fn(MouseEvent)>,
	on_wheel: Closure<dyn Fn(WheelEvent)>,
	on_commit_start: Closure<dyn Fn(CompositionEvent)>,
	on_commit_update: Closure<dyn Fn(CompositionEvent)>,
	on_commit_end: Closure<dyn Fn(CompositionEvent)>,
	resize_observer: ResizeObserver,
	dirty: Cell<bool>,
	nanos: Cell<i64>,
}
impl Drop for Win {
	fn drop(&mut self) {
		self.unbind()
	}
}
impl Win {
	pub fn win_ref(&'_ self) -> WinRef<'_> {
		WinRef(self)
	}
	pub fn rep(&self) -> &RcDynWinDelegate {
		&self.rep
	}
	pub fn dirty(&self, src: &str) {
		if false == self.dirty.get() {
			log::debug!("{} dirtied by {src}", &self.key_);
			self.dirty.set(true);
			self.frame.schedule();
		}
	}
	pub fn frame_ms(&self) -> i64 {
		const NANOS_PER_MS: i64 = 1000000;
		self.nanos.get() / NANOS_PER_MS
	}
	pub fn frame_nano(&self) -> i64 {
		self.nanos.get()
	}
}
impl Win {
	fn new(key: &str, win_hold: WinHold, rep: RcDynWinDelegate) -> Option<Self> {
		let window = web_sys::window()?;
		let document = window.document()?;
		let body = document.body()?;

		let cursor = Self::create_cursor(&document)?;

		let mut fill_window = false;
		let canvas: HtmlCanvasElement = if let Some(canvas) = document.get_element_by_id(key) {
			canvas.parent_element().map(|p| p.append_child(&cursor));
			canvas.dyn_into().ok()?
		} else {
			let canvas: HtmlCanvasElement = document.create_element("canvas").ok()?.dyn_into().ok()?;
			canvas.set_id(key);
			Self::sync_window_size_to_canvas(&window, &canvas);
			body.append_child(&canvas).ok()?;
			body.append_child(&cursor).ok()?;
			fill_window = true;
			canvas
		};

		let canvas_resize = Closure::new({
			let win_hold = win_hold.clone();
			move |_, _| {
				win_hold.with(|win| {
					use types::WinRef as _;
					win.rep().on_resize(win.win_ref(), win.win_ref().size());
					win.dirty("resize");
					Some(())
				});
			}
		});
		let resize_observer = ResizeObserver::new(canvas_resize.as_ref().unchecked_ref()).ok()?;
		resize_observer.observe(&canvas);

		Self::init_canvas(&canvas);

		let mut win = Self {
			key_: key.to_string(),
			rep,
			cursor,
			document,
			window: window.clone(),
			canvas: canvas.clone(),
			body,
			frame: Frame::new(window.clone(), {
				let win_hold = win_hold.clone();
				move |millis| {
					win_hold.with(|win| {
						win.dirty.set(false);
						win.nanos.set((millis * 1_000_000.) as i64);
						win.rep().on_req_draw(win.win_ref(), win.frame_nano());
						Some(())
					});
				}
			}),
			window_resize: None,
			_canvas_resize: canvas_resize,
			on_keydown: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: KeyboardEvent| win_hold.on_keyboard(evt)
			}),
			on_keyup: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: KeyboardEvent| win_hold.on_keyboard(evt)
			}),
			on_mousedown: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: MouseEvent| win_hold.on_mouse(evt)
			}),
			on_mouseup: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: MouseEvent| win_hold.on_mouse(evt)
			}),
			on_mousemove: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: MouseEvent| win_hold.on_mouse(evt)
			}),
			on_wheel: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: WheelEvent| win_hold.on_wheel(evt)
			}),
			on_commit_start: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: CompositionEvent| win_hold.on_commit_start(evt)
			}),
			on_commit_update: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: CompositionEvent| win_hold.on_commit_update(evt)
			}),
			on_commit_end: Closure::new({
				let win_hold = win_hold.clone();
				move |evt: CompositionEvent| win_hold.on_commit_end(evt)
			}),
			resize_observer,
			dirty: Cell::new(false),
			nanos: Cell::new(0),
		};
		win.dobind(fill_window);
		Some(win)
	}
	fn sync_window_size_to_canvas(window: &HtmlWindow, canvas: &HtmlCanvasElement) -> Option<()> {
		canvas.set_width(window.inner_width().ok()?.as_f64()? as _);
		canvas.set_height(window.inner_height().ok()?.as_f64()? as _);
		Some(())
	}
	fn bind_window_resize(&mut self) {
		let window_resized = {
			let window = self.window.clone();
			let canvas = self.canvas.clone();
			Closure::new(move || {
				Self::sync_window_size_to_canvas(&window, &canvas);
			})
		};
		let window_resized = Rc::new(window_resized);
		let window_resize = Closure::new({
			let window = self.window.clone();
			let resized = window_resized.clone();
			struct IdHold(Cell<Option<i32>>);
			impl Drop for IdHold {
				fn drop(&mut self) {
					if let Some(window) = web_sys::window() {
						self.0.take().map(|id| window.clear_timeout_with_handle(id));
					}
				}
			}
			let id_hold = IdHold(Default::default());
			move || {
				id_hold.0.take().map(|id| window.clear_timeout_with_handle(id));
				let fun = resized.as_ref().as_ref().unchecked_ref();
				let id = window.set_timeout_with_callback_and_timeout_and_arguments_0(fun, 100).ok();
				id_hold.0.set(id);
			}
		});
		self.window.set_onresize(Some(window_resize.as_ref().unchecked_ref()));
		self.window_resize = Some((window_resize, window_resized));
	}
	fn dobind(&mut self, fill_window: bool) {
		let canvas = &self.canvas;
		canvas.set_onkeydown(Some(self.on_keydown.as_ref().unchecked_ref()));
		canvas.set_onkeyup(Some(self.on_keyup.as_ref().unchecked_ref()));
		canvas.set_onmousedown(Some(self.on_mousedown.as_ref().unchecked_ref()));
		canvas.set_onmouseup(Some(self.on_mouseup.as_ref().unchecked_ref()));
		canvas.set_onmousemove(Some(self.on_mousemove.as_ref().unchecked_ref()));
		canvas.set_onwheel(Some(self.on_wheel.as_ref().unchecked_ref()));

		let cursor = &self.cursor;
		_ = cursor.add_event_listener_with_callback("compositionstart", self.on_commit_start.as_ref().unchecked_ref());
		_ = cursor.add_event_listener_with_callback("compositionupdate", self.on_commit_update.as_ref().unchecked_ref());
		_ = cursor.add_event_listener_with_callback("compositionend", self.on_commit_end.as_ref().unchecked_ref());

		if fill_window {
			self.bind_window_resize()
		}
	}
	fn unbind(&mut self) {
		self.frame.cancel();
		self.resize_observer.disconnect();
		if self.window_resize.is_some() {
			self.window.set_onresize(None);
			self.window_resize = None;
		}

		let canvas = &self.canvas;
		canvas.set_onkeydown(None);
		canvas.set_onkeyup(None);
		canvas.set_onmousedown(None);
		canvas.set_onmouseup(None);
		canvas.set_onmousemove(None);
		canvas.set_onwheel(None);

		let cursor = &self.cursor;
		_ = cursor.remove_event_listener_with_callback("compositionstart", self.on_commit_start.as_ref().unchecked_ref());
		_ = cursor.remove_event_listener_with_callback("compositionupdate", self.on_commit_update.as_ref().unchecked_ref());
		_ = cursor.remove_event_listener_with_callback("compositionend", self.on_commit_end.as_ref().unchecked_ref());
	}
	fn init_canvas(canvas: &HtmlCanvasElement) {
		_ = canvas.style().set_property("position", "absolute");
		canvas.set_tab_index(0);
	}
	fn create_cursor(document: &Document) -> Option<HtmlTextAreaElement> {
		let cursor: HtmlTextAreaElement = document.create_element("textarea").ok()?.dyn_into().ok()?;
		_ = cursor.set_attribute("autocorrect", "off");
		_ = cursor.set_attribute("autocapitalize", "off");
		_ = cursor.set_attribute("autocomplete", "off");
		_ = cursor.set_attribute("wrap", "off");
		_ = cursor.set_attribute("spellcheck", "false");
		let style = cursor.style();
		_ = style.set_property("position", "absolute");
		_ = style.set_property("background-color", "transparent");
		_ = style.set_property("border", "none");
		_ = style.set_property("resize", "none");
		_ = style.set_property_with_priority("outline", "none", "important");
		_ = style.set_property("width", "20px");
		_ = style.set_property("height", "20px");
		_ = style.set_property("line-height", "20px");
		_ = style.set_property("margin", "0");
		_ = style.set_property("padding", "0");
		_ = style.set_property("overflow", "hidden");
		Some(cursor)
	}
}

#[derive(Default, Clone)]
pub struct WinHold(Rc<Cell<Option<Index>>>);
impl WinHold {
	fn with<T>(&self, mut fun: impl FnMut(&Win) -> Option<T>) -> Option<T> {
		if let Some(key) = self.0.get() {
			with_win(key, |win| fun(win))
		} else {
			None
		}
	}
	fn on_keyboard(&self, evt: KeyboardEvent) {
		self.with(|win| {
			win.rep().on_keyboard(win.win_ref(), WinKeyboard(&evt));
			Some(())
		});
	}
	fn on_mouse(&self, evt: MouseEvent) {
		self.with(|win| {
			win.rep().on_mouse(win.win_ref(), WinMouse(&evt));
			Some(())
		});
	}
	fn on_wheel(&self, evt: WheelEvent) {
		self.with(|win| {
			win.rep().on_wheel(win.win_ref(), WinWheel(&evt));
			Some(())
		});
	}
	fn on_commit_start(&self, _evt: CompositionEvent) {
		self.with(|win| {
			log::warn!("oncommitstart {:?}", _evt.data());
			win.rep().on_ime(win.win_ref(), &types::Ime::begin());
			Some(())
		});
	}
	fn on_commit_update(&self, evt: CompositionEvent) {
		self.with(|win| {
			log::warn!("oncommitupdate {:?}", evt.data());
			let data = evt.data().unwrap_or_default();
			win.rep().on_ime(win.win_ref(), &types::Ime::update(data.into()));
			Some(())
		});
	}
	fn on_commit_end(&self, evt: CompositionEvent) {
		self.with(|win| {
			log::warn!("oncommitend {:?}", evt.data());
			let data = evt.data().unwrap_or_default();
			win.rep().on_ime(win.win_ref(), &types::Ime::end(data.into()));
			Some(())
		});
	}
}

pub type Key = Index;
thread_local! {
	static CACHE:RefCell<Arena<Win>> = Default::default();
}

pub fn new_window(key: impl AsRef<str>, rep: impl types::WinRep<Window> + 'static) -> Option<WinHandle> {
	let key = key.as_ref();
	CACHE.with_borrow_mut(|vars| {
		for (idx, win) in vars.iter() {
			if win.key_ == key.as_ref() {
				log::warn!("Duplicated canvas with same key");
				return Some(WinHandle(idx));
			}
		}

		let win_hold = WinHold::default();
		let key = vars.insert(Win::new(key, win_hold.clone(), Rc::new(rep))?);
		win_hold.0.set(Some(key));

		if let Some(win) = vars.get(key) {
			win.rep().on_create(win.win_ref());
			use types::WinRef as _;
			win.rep().on_resize(win.win_ref(), win.win_ref().size());
			win.dirty("new");
		}
		Some(WinHandle(key))
	})
}
pub fn del_window(key: Key) {
	CACHE.with_borrow_mut(|vars| {
		let ret = vars.remove(key);
		std::mem::drop(ret);
	})
}
pub(crate) fn with_win<T>(key: Key, fun: impl FnOnce(&Win) -> Option<T>) -> Option<T> {
	CACHE.with_borrow(|vars| vars.get(key).map(|win| fun(win)).flatten())
}
// pub(crate) fn with_win_mut<T>(key: Key, mut fun: impl FnMut(&mut Win) -> Option<T>) -> Option<T> {
// 	CACHE.with_borrow_mut(|vars| vars.get_mut(key).map(|win| fun(win)).flatten())
// }
pub(crate) fn with_wins<T>(mut fun: impl FnMut(&Arena<Win>) -> T) -> T {
	CACHE.with_borrow(|vars: &Arena<Win>| fun(vars))
}

struct Frame(HtmlWindow, Rc<Cell<Option<i32>>>, Closure<dyn FnMut(f64)>);
impl Frame {
	pub fn new(window: HtmlWindow, mut fun: impl FnMut(f64) + 'static) -> Self {
		let handle: Rc<Cell<Option<i32>>> = Default::default();
		Self(
			window,
			handle.clone(),
			Closure::new(move |millis: f64| {
				handle.set(None);
				fun(millis)
			}),
		)
	}
	pub fn schedule(&self) -> Option<()> {
		self.cancel();
		let handle = self.0.request_animation_frame(self.2.as_ref().unchecked_ref()).ok()?;
		self.1.set(Some(handle));
		Some(())
	}
	pub fn cancel(&self) {
		if let Some(id) = self.1.take() {
			_ = self.0.cancel_animation_frame(id);
		}
	}
}
impl Drop for Frame {
	fn drop(&mut self) {
		self.cancel()
	}
}
